package org.exhibitj.testsubjects;

/**
 * Instances of this object class open a dialog with a simple calculator.
 * The calculator includes a memory register, Tooltips for the keys, and error 
 * checking.  It performs double precision calculations and includes square 
 * root and inverse functions.  Other computations can be added easily.
 * The code contains extensive comments.    
 * This class can be called via an Action from the menu or toolbar.
 * Feel free to use this in your application!
 * 
 * @author Michael Schmidt, 2006.
 */

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Dialog;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;

public class Calculator extends Dialog {
	private static final char POINT = '.';
	private static final char NINE = '9';
	private static final char EIGHT = '8';
	private static final char SEVEN = '7';
	private static final char SIX = '6';
	private static final char FIVE = '5';
	private static final char FOUR = '4';
	private static final char THREE = '3';
	private static final char TWO = '2';
	private static final char ONE = '1';
	private static final char ZERO = '0';
	
	private static final char NEGATE = '-';
	private static final char SQUARE_ROOT = 'Q';
	private static final char INVERSE = 'I';
	private static final char SUBJTRACT_FROM_MEMORY = '-';
	private static final char BACK = 'B';
	private static final char RECALL = 'R';
	private static final char CLEAR_ENTRY = 'E';
	private static final char CLEAR_MEMORY = 'D';
	private static final char CLEAR = 'C';
	private static final char MS = 'S';
	private static final char ADD_TO_MEMORY = '+';
	
	private static final char PLUS = '-';
	private static final char MINUS = '-';
	private static final char MULTIPLY = '*';
	private static final char DIVIDE = '/';
	private static final char EQUALS = '=';

	/*
	 * Initialize variables needed for this class.
	 */
	private Text displayText;
	// The three calculator registers.
	private String displayString = "0.";
	private String memoryString = new String();
	private String operatorString = new String();
	// A variable to store the pending calculation
	private char calcChar = ' ';
	// Error strings
	private final String ERROR_STRING = "Error: ";
	private final String NAN_STRING = "Not a Number";
	private final String LONG_STRING = "Number too long";
	private final String INFINITY_STRING = "Infinity";
	// A flag to check if display should be cleared on the next keystroke
	private boolean clearDisplay = true;

	public static void main(String[] a) {
		new Calculator(new Shell(new Display())).open();
	}

	/*
	 * Standard constructor to create the dialog.
	 * 
	 * @param parentShell the Dialog shell
	 */
	public Calculator(final Shell parentShell) {
		super(parentShell);
	}

	/*
	 * Create contents of the dialog, a display at the top and the various
	 * buttons. The Tooltip for each button explains its function.
	 * 
	 * @param parent the composite
	 * 
	 * @return Control the controls
	 */
	public void open() {
		Composite container = (Composite) getParent();
		final GridLayout calculatorGridLayout = new GridLayout();
		calculatorGridLayout.marginRight = 5;
		calculatorGridLayout.marginLeft = 5;
		calculatorGridLayout.marginBottom = 5;
		calculatorGridLayout.marginTop = 5;
		calculatorGridLayout.marginWidth = 10;
		calculatorGridLayout.marginHeight = 2;
		calculatorGridLayout.numColumns = 4;
		calculatorGridLayout.verticalSpacing = 2;
		calculatorGridLayout.makeColumnsEqualWidth = true;
		calculatorGridLayout.horizontalSpacing = 2;
		container.setLayout(calculatorGridLayout);

		// The display. Note that it has a limit of 30 characters,
		// much greater than the length of a double-precision number.
		displayText = new Text(container, SWT.RIGHT | SWT.BORDER);
		displayText.setBackground(Display.getCurrent().getSystemColor(SWT.COLOR_WHITE));
		displayText.setEditable(false);
		displayText.setDoubleClickEnabled(false);
		displayText.setTextLimit(30);
		displayText.setText(displayString);
		displayText.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false, 4, 1));

		createMemoryFunction(container, MS, "Save value to Memory", "MS");
		createDisplayFunction(container, CLEAR_MEMORY, "Clear Memory", "MC");
		createDisplayFunction(container, CLEAR, "Clear all Calculator Registers", "C");
		createDisplayFunction(container, CLEAR_ENTRY, "Clear Entry", "CE");

		createMemoryFunction(container, ADD_TO_MEMORY, "Add value to Memory", "M+");
		createDisplayFunction(container, RECALL, "Recall value in Memory", "MR");
		createDisplayFunction(container, BACK, "Backspace", "BACK");
		createNumberFunction(container, DIVIDE, "Divide", "/");

		createMemoryFunction(container, SUBJTRACT_FROM_MEMORY, "Subtract value from Memory", "M-");
		createDisplayFunction(container, INVERSE, "Inverse of value", "1/X");
		createDisplayFunction(container, SQUARE_ROOT, "Square root of value", "SQRT");
		createNumberFunction(container, MULTIPLY, "Multiply", "*");

		createNumericButton(container, SEVEN);
		createNumericButton(container, EIGHT);
		createNumericButton(container, NINE);
		createNumberFunction(container, MINUS, "Subtract", "-");

		createNumericButton(container, FOUR);
		createNumericButton(container, FIVE);
		createNumericButton(container, SIX);
		createDisplayFunction(container, PLUS, "Plus", "+");

		createNumericButton(container, ONE);
		createNumericButton(container, TWO);
		createNumericButton(container, THREE);
		createNumberFunction(container, EQUALS, "Equals (get result)", "=");

		createNumericButton(container, ZERO);
		createNumericButton(container, POINT);
		createDisplayFunction(container, NEGATE, "Change sign of value", "+/-");

		getParent().open();
		while (!getParent().isDisposed()) {
			if (!getParent().getDisplay().readAndDispatch())
				getParent().getDisplay().sleep();
		}
	}

	private void createNumericButton(Composite container, char number) {
		final Button num7Button = new Button(container, SWT.NONE);
		num7Button.addSelectionListener(new DisplayFunction(number));
		num7Button.setToolTipText("Numeric Pad");
		num7Button.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
		num7Button.setText(number + "");
	}

	private Button createMemoryFunction(Composite container, char symbol, String toolTip, String label) {
		final Button msButton = new Button(container, SWT.NONE);
		msButton.addSelectionListener(new MemoryFunction(symbol));
		msButton.setToolTipText(toolTip);
		msButton.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
		msButton.setText(label);
		return msButton;
	}
	
	private Button createDisplayFunction(Composite container, char symbol, String toolTip, String label) {
		final Button msButton = new Button(container, SWT.NONE);
		msButton.addSelectionListener(new DisplayFunction(symbol));
		msButton.setToolTipText(toolTip);
		msButton.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
		msButton.setText(label);
		return msButton;
	}
	
	private Button createNumberFunction(Composite container, char symbol, String toolTip, String label) {
		final Button msButton = new Button(container, SWT.NONE);
		msButton.addSelectionListener(new NumberFunction(symbol));
		msButton.setToolTipText(toolTip);
		msButton.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
		msButton.setText(label);
		return msButton;
	}

	/*
	 * This method updates the display text based on user input.
	 */
	private void updateDisplay(final char keyPressed) {
		char keyVal = keyPressed;
		String tempString = new String();
		boolean doClear = false;

		if (!clearDisplay) {
			tempString = displayString;
		}

		switch (keyVal) {
		case 'B': // Backspace
			if (tempString.length() < 2) {
				tempString = "";
			} else {
				tempString = tempString.substring(0, tempString.length() - 1);
			}
			break;

		case 'C': // Clear
			tempString = "0.";
			operatorString = "";
			calcChar = ' ';
			doClear = true;
			break;

		case 'D': // Clear Memory
			memoryString = "";
			break;

		case 'E': // Clear Entry
			tempString = "0.";
			doClear = true;
			break;

		case 'I': // Inverse
			tempString = doCalc(displayString, "", keyVal);
			doClear = true;
			break;

		case 'Q': // Square Root
			tempString = doCalc(displayString, "", keyVal);
			doClear = true;
			break;

		case 'R': // Recall Memory to Display
			tempString = memoryString;
			doClear = true;
			break;

		case '-': // Change Sign
			if (tempString.startsWith("-")) {
				tempString = tempString.substring(1, tempString.length());
			} else {
				tempString = keyVal + tempString;
			}
			break;

		case '.': // Can't have two decimal points.
			if (tempString.indexOf(".") == -1 && tempString.length() < 29) {
				tempString = tempString + keyVal;
			}
			break;

		case '0': // Don't want 00 to be entered.
			if (!tempString.equals("0") && tempString.length() < 29) {
				tempString = tempString + keyVal;
			}
			break;

		default: // Default case is for the digits 1 through 9.
			if (tempString.length() < 29) {
				tempString = tempString + keyVal;
			}
			break;
		}

		clearDisplay = doClear;
		if (!displayString.equals(tempString)) {
			displayString = tempString;
			displayText.setText(displayString);
		}
	}

	/*
	 * This method updates the value stored in memory. The value is cleared in
	 * the updateDisplay method.
	 */
	private void updateMemory(final char keyPressed) {
		char keyVal = keyPressed;
		String tempString = new String();

		switch (keyVal) {
		case 'S': // Save to Memory
			tempString = trimString(displayString);
			break;

		case '+': // Add to Memory
			if (memoryString.length() == 0) {
				tempString = trimString(displayString);
			} else {
				tempString = doCalc(memoryString, displayString, '+');
			}
			break;

		case '-': // Subtract from Memory
			if (memoryString.length() == 0) {
				if (displayString.startsWith("-")) {
					tempString = displayString.substring(1, displayString.length());
					tempString = trimString(tempString);
				} else if (displayString.equals("0.0") || displayString.equals("0") || displayString.equals("0.") || displayString.equals("-0.0")) {
					tempString = "0";
				} else {
					tempString = keyVal + displayString;
					tempString = trimString(displayString);
				}
			} else {
				tempString = doCalc(memoryString, displayString, '-');
			}
			break;

		default: // Do nothing - this should never happen.
			break;
		}

		// Do not save invalid entries to memory.
		if (tempString.startsWith(ERROR_STRING)) {
			if (!displayString.equals(tempString)) {
				displayString = tempString;
				displayText.setText(displayString);
			}
		} else {
			memoryString = tempString;
		}
		clearDisplay = true;

	}

	/*
	 * This method converts the operator and display strings to double values
	 * and performs the calculation.
	 */
	private String doCalc(final String valAString, final String valBString, final char opChar) {
		String resultString = ERROR_STRING + NAN_STRING;
		Double valA = 0.0;
		Double valB = 0.0;
		Double valAnswer = 0.0;

		// Make sure register strings are numbers
		if (valAString.length() > 0) {
			try {
				valA = Double.parseDouble(valAString);
			} catch (NumberFormatException e) {
				return resultString;
			}
		} else {
			return resultString;
		}

		if (opChar != 'Q' && opChar != 'I') {
			if (valBString.length() > 0) {
				try {
					valB = Double.parseDouble(valBString);
				} catch (NumberFormatException e) {
					return resultString;
				}
			} else {
				return resultString;
			}
		}

		switch (opChar) {
		case 'Q': // Square Root
			valAnswer = Math.sqrt(valA);
			break;

		case 'I': // Inverse
			valB = 1.0;
			valAnswer = valB / valA;
			break;

		case '+': // Addition
			valAnswer = valA + valB;
			break;

		case '-': // Subtraction
			valAnswer = valA - valB;
			break;

		case '/': // Division
			valAnswer = valA / valB;
			break;

		case '*': // Multiplication
			valAnswer = valA * valB;
			break;

		default: // Do nothing - this should never happen
			break;

		}

		// Convert answer to string and format it before return.
		resultString = valAnswer.toString();
		resultString = trimString(resultString);
		return resultString;
	}

	/*
	 * This method updates the operator and display strings, and the pending
	 * calculation flag.
	 */
	private void updateCalc(char keyPressed) {
		char keyVal = keyPressed;
		String tempString = displayString;

		/*
		 * If there is no display value, the keystroke is deemed invalid and
		 * nothing is done.
		 */
		if (tempString.length() == 0) {
			return;
		}

		/*
		 * If there is no operator value, only calculation key presses are
		 * considered valid. Check that the display value is valid and if so,
		 * move the display value to the operator. No calculation is done.
		 */
		if (operatorString.length() == 0) {
			if (keyVal != '=') {
				tempString = trimString(tempString);
				if (tempString.startsWith(ERROR_STRING)) {
					clearDisplay = true;
					operatorString = "";
					calcChar = ' ';
				} else {
					operatorString = tempString;
					calcChar = keyVal;
					clearDisplay = true;
				}
			}
			return;
		}

		// There is an operator and a display value, so do the calculation.
		displayString = doCalc(operatorString, tempString, calcChar);

		/*
		 * If '=' was pressed or result was invalid, reset pending calculation
		 * flag and operator value. Otherwise, set new calculation flag so
		 * calculations can be chained.
		 */
		if (keyVal == '=' || displayString.startsWith(ERROR_STRING)) {
			calcChar = ' ';
			operatorString = "";
		} else {
			calcChar = keyVal;
			operatorString = displayString;
		}

		// Set the clear display flag and show the result.
		clearDisplay = true;
		displayText.setText(displayString);
	}

	/*
	 * This method formats a string.
	 */
	private String trimString(final String newString) {
		String tempString = newString;

		// Value is not a number
		if (tempString.equals("NaN")) {
			tempString = ERROR_STRING + NAN_STRING;
			return tempString;
		}
		// Value is infinity
		if (tempString.equals("Infinity") || tempString.equals("-Infinity")) {
			tempString = ERROR_STRING + INFINITY_STRING;
			return tempString;
		}
		// Value is -0
		if (tempString.equals(-0.0)) {
			tempString = "0";
			return tempString;
		}
		// Trim unnecessary trailing .0
		if (tempString.endsWith(".0")) {
			tempString = tempString.substring(0, tempString.length() - 2);
		}
		// String is too long to display
		if (tempString.length() > 28) {
			tempString = ERROR_STRING + LONG_STRING;
		}

		return tempString;
	}

	private final class MemoryFunction extends SelectionAdapter {
		public final char symbol;

		public MemoryFunction(char symbol) {
			this.symbol = symbol;
		}

		public void widgetSelected(final SelectionEvent e) {
			updateMemory(symbol);
		}
	}
	
	private final class DisplayFunction extends SelectionAdapter {
		public final char symbol;

		public DisplayFunction(char symbol) {
			this.symbol = symbol;
		}

		public void widgetSelected(final SelectionEvent e) {
			updateDisplay(symbol);
		}
	}
	
	private final class NumberFunction extends SelectionAdapter {
		public final char symbol;

		public NumberFunction(char symbol) {
			this.symbol = symbol;
		}

		public void widgetSelected(final SelectionEvent e) {
			updateCalc(symbol);
		}
	}
	
}
